/**
 * 
 */
package qy.jalgotrade.technical;

import java.time.ZonedDateTime;
import java.util.Map;

import com.google.common.math.DoubleMath;

import qy.jalgotrade.dataseries.DataSeries;
import qy.jalgotrade.dataseries.SequenceDataSeries;
import qy.jalgotrade.event.EventHandler;
import qy.jalgotrade.technical.ma.SMA;
import qy.jalgotrade.technical.stats.StdDev;
import qy.jalgotrade.utils.CommonUtils;

/**
 * Bollinger Bands filter as described in
 * http://stockcharts.com/school/doku.php?id=chart_school:technical_indicators:bollinger_bands.
 * 
 * @author qy
 *
 */
public class BollingerBands implements EventHandler {

	private SMA __sma;

	private StdDev __stdDev;

	private SequenceDataSeries<Double> __upperBand;

	private SequenceDataSeries<Double> __lowerBand;

	private double __numStdDev;

	// XXX: qy-pyalgotrade: numStdDev2, numStdDev3:

	private double __numStdDev2;

	private double __numStdDev3;

	private SequenceDataSeries<Double> __upperBand2;

	private SequenceDataSeries<Double> __lowerBand2;

	private SequenceDataSeries<Double> __upperBand3;

	private SequenceDataSeries<Double> __lowerBand3;

	// XXX: qy-pyalgotrade: BW, b%:

	private SequenceDataSeries<Double> __bandwidth;

	private SequenceDataSeries<Double> __bPercent;

	/**
	 * 
	 * @param dataSeries The DataSeries instance being filtered.
	 * @param period     The number of values to use in the calculation. Must be > 1.
	 * @param numStdDev  The number of standard deviations to use for the upper and lower bands.
	 * @throws Exception
	 */
	public BollingerBands(SequenceDataSeries<Double> dataSeries, int period, double numStdDev) throws Exception {

		this(dataSeries, period, numStdDev, Double.NaN, Double.NaN, 0, 0);
	}

	/**
	 * 
	 * @param dataSeries The DataSeries instance being filtered.
	 * @param period     The number of values to use in the calculation. Must be > 1.
	 * @param numStdDev  The number of standard deviations to use for the upper and lower bands.
	 * @param numStdDev2 The number of standard deviations to use for the upper2 and lower2 bands.
	 * @throws Exception
	 */
	public BollingerBands(SequenceDataSeries<Double> dataSeries, int period, double numStdDev, double numStdDev2)
	        throws Exception {

		this(dataSeries, period, numStdDev, numStdDev2, Double.NaN, 0, 0);
	}

	/**
	 * 
	 * @param dataSeries The DataSeries instance being filtered.
	 * @param period     The number of values to use in the calculation. Must be > 1.
	 * @param numStdDev  The number of standard deviations to use for the upper and lower bands.
	 * @param numStdDev2 The number of standard deviations to use for the upper2 and lower2 bands.
	 * @param numStdDev3 The number of standard deviations to use for the upper3 and lower3 bands.
	 * @throws Exception
	 */
	public BollingerBands(SequenceDataSeries<Double> dataSeries, int period, double numStdDev, double numStdDev2,
	        double numStdDev3) throws Exception {

		this(dataSeries, period, numStdDev, numStdDev2, numStdDev3, 0, 0);
	}

	/**
	 * 
	 * @param dataSeries The DataSeries instance being filtered.
	 * @param period     The number of values to use in the calculation. Must be > 1.
	 * @param numStdDev  The number of standard deviations to use for the upper and lower bands.
	 * @param numStdDev2 The number of standard deviations to use for the upper2 and lower2 bands.
	 * @param numStdDev3 The number of standard deviations to use for the upper3 and lower3 bands.
	 * @param ddof       Means Delta Degrees of Freedom. The divisor used in calculations is N - ddof,
	 *                   where N represents the number of elements. By default ddof is zero.
	 * @throws Exception
	 */
	public BollingerBands(SequenceDataSeries<Double> dataSeries, int period, double numStdDev, double numStdDev2,
	        double numStdDev3, int ddof) throws Exception {

		this(dataSeries, period, numStdDev, numStdDev2, numStdDev3, ddof, 0);
	}

	/**
	 * 
	 * @param dataSeries The DataSeries instance being filtered.
	 * @param period     The number of values to use in the calculation. Must be > 1.
	 * @param numStdDev  The number of standard deviations to use for the upper and lower bands.
	 * @param numStdDev2 The number of standard deviations to use for the upper2 and lower2 bands.
	 * @param numStdDev3 The number of standard deviations to use for the upper3 and lower3 bands.
	 * @param ddof       Means Delta Degrees of Freedom. The divisor used in calculations is N - ddof,
	 *                   where N represents the number of elements. By default ddof is zero.
	 * @param maxLen     The maximum number of values to hold.
	 * @throws Exception
	 */
	public BollingerBands(SequenceDataSeries<Double> dataSeries, int period, double numStdDev, double numStdDev2,
	        double numStdDev3, int ddof, int maxLen) throws Exception {

		if (maxLen == 0) {
			assert period < DataSeries.DEFAULT_MAX_LEN;
		} else {
			assert period < maxLen;
		}
		__sma = new SMA(dataSeries, period, maxLen);
		__stdDev = new StdDev(dataSeries, period, ddof, maxLen);
		__upperBand = new SequenceDataSeries<>(maxLen, Double.class);
		__lowerBand = new SequenceDataSeries<>(maxLen, Double.class);
		__numStdDev = numStdDev;
		__numStdDev2 = numStdDev2;
		__numStdDev3 = numStdDev3;
		__upperBand2 = null;
		__lowerBand2 = null;
		__upperBand3 = null;
		__lowerBand3 = null;
		if (!Double.isNaN(__numStdDev2)) {
			__upperBand2 = new SequenceDataSeries<>(maxLen, Double.class);
			__lowerBand2 = new SequenceDataSeries<>(maxLen, Double.class);
		}
		if (!Double.isNaN(__numStdDev3)) {
			__upperBand3 = new SequenceDataSeries<>(maxLen, Double.class);
			__lowerBand3 = new SequenceDataSeries<>(maxLen, Double.class);
		}
		__bandwidth = new SequenceDataSeries<>(maxLen, Double.class);
		__bPercent = new SequenceDataSeries<>(maxLen, Double.class);
		// It is important to subscribe after sma and stddev since we'll use those values.
		dataSeries.getNewValueEvent().subscribe(this);
	}

	/*
	 * (non-Javadoc)
	 * 
	 * @see qy.jalgotrade.event.EventHandler#handle(java.lang.String, java.util.Map)
	 */
	@Override
	public void handle(String eventName, Map<String, Object> params) throws Exception {

		@SuppressWarnings("unchecked")
		SequenceDataSeries<Double> dataSeries = (SequenceDataSeries<Double>) params.get("dataSeries");
		ZonedDateTime dateTime = (ZonedDateTime) params.get("dateTime");
		double value = (double) params.get("value");
		__onNewValue(dataSeries, dateTime, value);
	}

	/**
	 * 
	 * @param dataSeries
	 * @param dateTime
	 * @param value
	 * @throws Exception
	 */
	private void __onNewValue(SequenceDataSeries<Double> dataSeries, ZonedDateTime dateTime, double value)
	        throws Exception {

		double upperValue = Double.NaN;
		double lowerValue = Double.NaN;
		double upperValue2 = Double.NaN;
		double lowerValue2 = Double.NaN;
		double upperValue3 = Double.NaN;
		double lowerValue3 = Double.NaN;
		double bandwidthValue = Double.NaN;
		double bPercentValue = Double.NaN;

		if (!Double.isNaN(value)) {
			double sma = __sma.__getitem__(-1);
			if (!Double.isNaN(sma)) {
				double stdDev = __stdDev.__getitem__(-1);
				upperValue = sma + stdDev * __numStdDev;
				lowerValue = sma - stdDev * __numStdDev;
				// XXX: qy-pyalgotrade: numStdDev2, numStdDev3:
				if (!Double.isNaN(__numStdDev2)) {
					upperValue2 = sma + stdDev * __numStdDev2;
					lowerValue2 = sma - stdDev * __numStdDev2;
				}
				if (!Double.isNaN(__numStdDev3)) {
					upperValue3 = sma + stdDev * __numStdDev3;
					lowerValue3 = sma - stdDev * __numStdDev3;
				}
				// XXX: qy-pyalgotrade: BW, b%:
				bandwidthValue = (upperValue - lowerValue) / sma;
				if (DoubleMath.fuzzyEquals(upperValue, lowerValue, CommonUtils.FUZZY_EQUAL_TOLERANCE_MATH)) {
					bPercentValue = 0.5;
				} else {
					bPercentValue = (value - lowerValue) / (upperValue - lowerValue);
				}
			}
		}

		__upperBand.appendWithDateTime(dateTime, upperValue);
		__lowerBand.appendWithDateTime(dateTime, lowerValue);
		if (!Double.isNaN(__numStdDev2)) {
			__upperBand2.appendWithDateTime(dateTime, upperValue2);
			__lowerBand2.appendWithDateTime(dateTime, lowerValue2);
		}
		if (!Double.isNaN(__numStdDev3)) {
			__upperBand3.appendWithDateTime(dateTime, upperValue3);
			__lowerBand3.appendWithDateTime(dateTime, lowerValue3);
		}
		// XXX: qy-pyalgotrade: BW, b%:
		__bandwidth.appendWithDateTime(dateTime, bandwidthValue);
		__bPercent.appendWithDateTime(dateTime, bPercentValue);
	}

	/**
	 * Returns the upper band as a :class:`pyalgotrade.dataseries.DataSeries`.
	 * 
	 * @return
	 */
	public SequenceDataSeries<Double> getUpperBand() {

		return __upperBand;
	}

	public SequenceDataSeries<Double> getMiddleBand() {

		return __sma;
	}

	public SequenceDataSeries<Double> getLowerBand() {

		return __lowerBand;
	}

	// XXX: qy-pyalgotrade: numStdDev2, numStdDev3:

	public SequenceDataSeries<Double> getUpperBand2() {

		return __upperBand2;
	}

	public SequenceDataSeries<Double> getLowerBand2() {

		return __lowerBand2;
	}

	public SequenceDataSeries<Double> getUpperBand3() {

		return __upperBand3;
	}

	public SequenceDataSeries<Double> getLowerBand3() {

		return __lowerBand3;
	}

	// XXX: qy-pyalgotrade: BW, b%:

	public SequenceDataSeries<Double> getBandwidth() {

		return __bandwidth;
	}

	public SequenceDataSeries<Double> getBPercent() {

		return __bPercent;
	}
}
